Repository navigation
plugin/fragment: keep blocks in order under keyed x-for - #69
Conversation
Since Alpine 3.15.9, keyed x-for moves only the item element and, for x-if, its single rendered element. The content of x-fragment, x-when and x-match was rendered before the template and was left behind, so reordering mixed up the blocks. Render the content after the template, like x-if, and expose its last node through _x_currentIfEl and _x_lastRenderedEl, so x-for treats the whole block as the item. The shared mutation observer then restores the content of every block in the affected parent right after its template, following nested blocks and x-if. The observer also handles removed nodes and the records produced by deleting nested blocks, and works with Alpine versions that do not expose the last rendered item of x-for or destroy x-if content. Fixes #62
Cover reordering, adding, removing and toggling the items of a keyed x-for for x-fragment, x-when and x-match, checking the exact node layout and that the existing nodes are moved rather than recreated. For x-fragment, check all transitions between the orders of four items. Also cover nested blocks, x-if inside a block and vice versa, a block containing a keyed x-for of blocks, restoring a moved node of a block, keeping the initial content of nested x-if and x-for, and that the observer settles without a feedback loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
📦 Bundle size comparison
|
|
This fix fails specific tests: <div x-data="{ outer: [1, 2], inner: [1, 2] }">
<button @click="outer = [2, 1]">outer = [2, 1]</button>
<div id="target">
<template x-for="o in outer" :key="o">
<template x-fragment>
<h2 x-text="o"></h2>
<template x-for="n in inner" :key="n">
<b x-text="o + '-' + n"></b>
</template>
<footer>F</footer>
</template>
</template>
</div>
</div>After
the inner loop's |
…blocks Alpine before 3.14.2 removes x-if content and x-for items without destroying them, leaving that to its mutation observer. Deleting a block within mutateDom paused the observer, so the items of x-for inside x-if, and the content of x-if items of x-for, stayed in the DOM. Delete the block outside of mutateDom, which also makes deleting the blocks left behind from the shared observer unnecessary.
|
I'd rather not fix in Keyed So I'd keep only what Alpine supports and the unsupported cases fail the same way as in Alpine. With alpinejs/alpine#4912 they work without any code on our side. |
Alpine's keyed x-for does not keep nested templates of its items in order when the list changes (alpinejs/alpine#4911). The previous changes worked around this for templates placed directly in a block, which took most of anchor_block: tracking the items of nested x-for lists and intercepting their lookups. This is a limitation of Alpine itself, and alpinejs/alpine#4912 is expected to fix it, so we no longer work around it. anchor_block now restores only the block's own nodes, and only when they are out of order. Content that a nested x-if, x-for or block renders between them is left as is. It is never lost or duplicated, but it can end up out of order, as with nested templates in plain Alpine x-for items. Mark the nested template reordering cases as test.fail, so they start failing once Alpine includes alpinejs/alpine#4912
Fixes #62